import asyncio
import json
import re
import threading
import time
from json import JSONDecodeError

import serial
import websockets
from pynput import keyboard
from pynput.keyboard import Key
from serial import PARITY_EVEN
from websockets.exceptions import ConnectionClosedError

"""
python 3.6 版本,打包64位
"""
socketIp = 'localhost'
socketPort = 8765
portHeight = "COM2"  # 身高串口
baudrateHeight = 115200  # 身高波特率

portPressure = "COM3"  # 血压串口
baudratePressure = 2400  # 身高波特率
timeout = 0.5

examNo = {'1': '', '2': '', '3': ''}  # 患者体检号,用户表示当前数据的归属
heightSer = None
pressureSer = None

CONNECTIONS = set()  # 存放已连接的客户端信息,以便广播消息
# 扫码枪扫描的体检号
scanExam = ''
keyboardLoop = None  # 新开一个loop,用于在同步代码中调用异步方法


async def broad_cast_msg(message, msg_type):
    """
    向客服端广播消息,消息发送之后将体检号置为空
    :param message: 消息体
    :param msg_type: 消息类型 身高为1, 血压为2,3为监听键盘输入结果
    :return:
    """
    global examNo
    print('broad_cast_msg', message, msg_type)
    try:
        if len(examNo) > 0:
            no = ''
            if str(msg_type) in examNo.keys():
                no = examNo[str(msg_type)]
            for ws in CONNECTIONS:
                await ws.send(json.dumps({"examNo": no, "type": msg_type, "data": message}))
            # websockets.broadcast(CONNECTIONS, json.dumps({"examNo": no, "type": msg_type, "data": message}))
            examNo[str(msg_type)] = ''
        else:
            for ws in CONNECTIONS:
                await ws.send(json.dumps({"examNo": "", "type": msg_type, "data": message}))
            # websockets.broadcast(CONNECTIONS, json.dumps({"examNo": "", "type": msg_type, "data": message}))
            examNo.clear()
    except Exception as e:
        print('error:', e)


# 打开身高串口
async def openHeightSerial():
    print('openHeightSerial')
    global heightSer
    if heightSer is None:
        try:
            heightSer = serial.Serial(port=portHeight,
                                      baudrate=baudrateHeight,
                                      timeout=timeout,
                                      bytesize=8)
        except Exception:
            return True
    if not heightSer.isOpen():  # 如果端口没有开启,则开启端口
        heightSer.open()
    heightSer.flushInput()
    heightSer.flushOutput()
    while True:
        data = heightSer.readall()  # 读取一个字节的数据
        try:
            if data:
                decoded_data = data.decode("unicode-escape")
                subData = decoded_data.split('\n')
                subData = [x for x in subData if 'cm' in x and 'kg' in x]
                if subData:
                    strData = subData[0]
                    h = re.findall(r'\d+\.?\d*[cm]+', strData)[0].strip('cm')
                    w = re.findall(r'\d+\.?\d*[kg]+', strData)[0].strip('kg')
                    send = h + ',' + w
                    # print('send:', send)
                    if send and CONNECTIONS:
                        await  broad_cast_msg(send, 1)
        except Exception as e:
            print('error:', e)


# 打开身高串口
async def openPressureSerial():
    print('openHeightSerial')
    global pressureSer
    if pressureSer is None:
        try:
            pressureSer = serial.Serial(port=portPressure,
                                        baudrate=baudratePressure,
                                        timeout=timeout,
                                        bytesize=7,
                                        parity=PARITY_EVEN)
        except Exception as e:
            print('error:', e)
            return True
    if not pressureSer.isOpen():  # 如果端口没有开启,则开启端口
        r = pressureSer.open()
        print('open:', r)
    pressureSer.flushInput()
    pressureSer.flushOutput()
    while True:
        data = pressureSer.readall()  # 读取一个字节的数据
        if data:
            try:
                decoded_data = data.decode("GBK")
                # print('decode_pressure:', decoded_data)
                if decoded_data and CONNECTIONS:
                    await  broad_cast_msg(decoded_data, 2)
            except Exception as e:
                print('出错:', e)


# 记录键盘输入时间
pressTime = 0


# 键盘按下事件
def keyPress(key):
    """
    如果pressTime=0 将当前时间赋值给pressTime,并且将保存的数据清空
    如果当前时间跟上次时间相差过长,则清空保存数据,同时将pressTime赋值0,并且不再走下面的流程
    如果有回车键输入,则判断pressTime 与当前时间差,如果时间过长,则清空保存数据,同时pressTime置为0
    如果时间差在允许范围内,则说明是扫码结果,将结果广播出去,清空数据,同时pressTime置为0
    :param key:
    :return:
    """
    global scanExam, pressTime, keyboardLoop
    # print('origin:',key,pressTime)
    if pressTime == 0:
        pressTime = time.time()
        scanExam = ''
    elif time.time() - pressTime > 0.5:
        # 如果两次按键间隔时间过长,则清除存储的数据
        scanExam = ''
        pressTime = 0
        return
    try:
        if hasattr(key, 'char') and key.char.isdigit():
            # print("key:",key.char)
            scanExam += key.char
        else:
            pressTime = 0
        if key == Key.enter and scanExam:
            print("examNo:", scanExam)
            if scanExam and re.match(r'^[0-9]*$', scanExam):
                coroutine = broad_cast_msg(scanExam, 3)
                if keyboardLoop is None:
                    keyboardLoop = asyncio.new_event_loop()
                asyncio.set_event_loop(keyboardLoop)
                future = asyncio.gather(coroutine)
                keyboardLoop.run_until_complete(future)
                scanExam = ''
                pressTime = 0
    except AttributeError as e:
        scanExam = ''
        pressTime = 0
        print('error:', e)
    except TypeError as e:
        pressTime = 0
        print("error:", e)


# 监听键盘输入
async def watchKeyboard():
    print('watchKeyboard')
    with keyboard.Listener(on_press=keyPress) as lsn:
        lsn.join()


# 监听客户端的消息
async def register(websocket, path):
    global examNo
    try:
        # Register user
        CONNECTIONS.add(websocket)

        async for message in websocket:
            print(message)

            try:
                event = json.loads(message)
                # print('json:', event)
                type_exam = event['type']
                if type_exam == 'ping':
                    await websocket.send('{"type":"pong"}')
                else:
                    no = event['examNo']
                    if type_exam and no and len(str(type_exam)) > 0 and len(str(no)) > 0:
                        examNo[str(type_exam)] = str(no)
            except JSONDecodeError as e:
                print('解析出错:', e)
            except KeyError as e:
                print('没有对应的key值', e)
            except ConnectionClosedError as e:
                print('error:', e)
    finally:
        # Unregister user
        CONNECTIONS.remove(websocket)
        for ws in CONNECTIONS:
            ws.send('close')


# 开启websocket 服务
async def main():
    async with websockets.serve(register, socketIp, socketPort):
        await asyncio.Future()


def thread_loop_height(loop):
    asyncio.set_event_loop(loop)
    future = asyncio.gather(openHeightSerial())
    loop.run_until_complete(future)


def thread_loop_pressure(loop):
    asyncio.set_event_loop(loop)
    future = asyncio.gather(openPressureSerial())
    loop.run_until_complete(future)


def thread_loop_keyboard(loop):
    asyncio.set_event_loop(loop)
    loop.run_until_complete(asyncio.gather(watchKeyboard()))


# 是否需要开启端口,如果不需要进行关闭,防止占用端口导致其他设备无法使用
openPort = True
if __name__ == '__main__':
    if openPort:
        # 开启一个新的线程监听身高串口信息
        thread_loop = asyncio.new_event_loop()
        t = threading.Thread(target=thread_loop_height, args=(thread_loop,))
        t.daemon = True
        t.start()

        # 开启一个新的线程监听血压串口信息
        pressure_loop = asyncio.new_event_loop()
        tp = threading.Thread(target=thread_loop_pressure, args=(pressure_loop,))
        tp.daemon = True
        tp.start()
    # 开启一个新线程监听键盘输入
    key_loop = asyncio.new_event_loop()
    tk = threading.Thread(target=thread_loop_keyboard, args=(key_loop,))
    tk.daemon = True
    tk.start()
    # asyncio.run(main())
    start_server = websockets.serve(register, socketIp, socketPort)

    asyncio.get_event_loop().run_until_complete(start_server)
    asyncio.get_event_loop().run_forever()
